﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data.Common;
using System.Data;
using System.Globalization;
using System.Text;
using System.Collections;
using System.Net;
using System.IO;
using Newtonsoft.Json;
using System.Collections.Specialized;
using System.Threading;
using ResourceBlender.Core;
using ResourceBlender.DAL;

/// <summary>
/// Translates resources from one language to another.
/// </summary>
public partial class MachineTranslate : LocalPage
{
    private Database db;
    private static Dictionary<int, PendingTranslation> resources = new Dictionary<int, PendingTranslation>();
    private static ManualResetEvent[] translationsDone = null;

    private static string sourceLanguage,
                          sourceType,
                          destinationLanguage,
                          destinationType;
    private static bool overwriteExisting,
                        verbose;
    private static int minWorkerThreads,
                       minIOThreads,
                       maxWorkerThreads,
                       maxIOThreads;

    /// <summary>
    /// Initializes the base and destination language listboxes.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
    protected void Page_Load(object sender, EventArgs e)
    {
        if(!Page.IsPostBack) {
            litAddAppLink.Text = ASPNETUtils.FormatLink(I18nUtils.GetString("UIStrings", "AddAppLink"), ResolveUrl("~/Applications.aspx"));
            litPageDescription.Text = ASPNETUtils.FormatLink(I18nUtils.GetString("UIStrings", "GoogleTranslateLink"), "http://www.google.co.uk/language_tools");

            db = new Database();
            db.OpenConnection();
            using(Command cmd = db.CreateCommand()) {
                cmd.CommandText = "SELECT id, displayname FROM " + db.TablePrefix + "applications ORDER BY displayname ASC";
                ResourceBlender.DAL.DataAdapter adapter = new ResourceBlender.DAL.DataAdapter(cmd);
                DataSet ds = new DataSet();
                adapter.Fill(ds);
                radApplication.DataSource = ds;
                radApplication.DataValueField = "id";
                radApplication.DataTextField = "displayname";
                radApplication.DataBind();
                if(radApplication.Items.Count > 0) {
                    radApplication.SelectedIndex = 0;
                    DataTable googleLanguages = new DataTable();
                    googleLanguages.Columns.Add("code");
                    googleLanguages.Columns.Add("englishName");
                    googleLanguages.DefaultView.Sort = "englishName";
                    foreach(KeyValuePair<string,string> language in GoogleTranslator.GoogleSupportedLanguages) {
                        googleLanguages.Rows.Add(language.Key, language.Value);
                    }
                    lstSourceType.DataSource = googleLanguages;
                    lstSourceType.DataTextField = "englishName";
                    lstSourceType.DataValueField = "code";
                    lstSourceType.DataBind();
                    lstSourceType.SelectedIndex = 0;
                    lstDestinationType.DataSource = googleLanguages;
                    lstDestinationType.DataTextField = "englishName";
                    lstDestinationType.DataValueField = "code";
                    lstDestinationType.DataBind();
                    lstDestinationType.SelectedIndex = 0;
                    ShowLanguages(null, EventArgs.Empty);
                    mltMain.ActiveViewIndex = 1;
                    Page.ClientScript.RegisterClientScriptInclude("blockUI", ResolveUrl("~/js/jquery.block.UI.min.js"));
                    ScriptManager.RegisterClientScriptBlock(this, typeof(Page), "ajaxBlock", @"$(function(){ var prm = Sys.WebForms.PageRequestManager.getInstance();
                        prm.add_endRequest(function() { $('#blockContainer').unblock(); $('#" + btnRefresh.ClientID + @"').remove(); });
                        prm.add_initializeRequest(function(sender, args) { $('#blockContainer').block({ message: ""Please Wait"" }); });
                        $.blockUI.defaults.css = {};
                        $('#notResponding').click(function() { $('#blockContainer').unblock(); return false; }); 
                        $('#" + btnRefresh.ClientID + @"').remove();
                        });
                        ", true);
                }
            }
            db.CloseConnection();
        }
    }


    /// <summary>
    /// Shows the languages and bundles for the application selected.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
    protected void ShowLanguages(object sender, EventArgs e) {
        if(radApplication.SelectedValue == null) {
            return;
        }
        db = new Database();
        db.OpenConnection();
        using(Command cmd = db.CreateCommand()) {
            cmd.CommandText = "SELECT languages, defaultLanguage FROM " + db.TablePrefix + "applications WHERE id = @p";
            cmd.AddParameterWithValue("@p", radApplication.SelectedValue);
            List<ListItem> tempSourceLanguages = new List<ListItem>(),
                           tempDestinationLanguages = new List<ListItem>(),
                           tempBundles = new List<ListItem>();
            string rawLanguages = null,
                   defaultLanguage = null,
                   defaultLanguageCode = string.Empty;
            DbDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow);
            if(!reader.Read()) {
                mltMain.ActiveViewIndex = 1;
                mltProgress.ActiveViewIndex = 1;
                LogText(string.Format(I18nUtils.GetString("Errors", "ErrorNumDescription"), "410", I18nUtils.GetString("Errors", "AppNotFoundName")), true);
                return;
            } else {
                rawLanguages = reader["languages"].ToString();
                defaultLanguage = reader["defaultLanguage"].ToString();
                reader.Close();
            }
            foreach(string abbrev in rawLanguages.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) {
                string englishName = Utils.GetSafeEnglishName(abbrev);
                ListItem itmSrc = new ListItem(englishName, abbrev);
                if(abbrev == defaultLanguage) {
                    itmSrc.Selected = true;
                    try {
                        defaultLanguageCode = GoogleTranslator.ISOCode2GoogleCode(CultureInfo.GetCultureInfo(abbrev).TwoLetterISOLanguageName);
                    } catch {
                        System.Diagnostics.Debug.WriteLine("Invalid language code for " + abbrev);
                    }
                }
                tempSourceLanguages.Add(itmSrc);
                tempDestinationLanguages.Add(new ListItem(englishName, abbrev));
            }
            ListComparer cmp = new ListComparer();
            tempSourceLanguages.Sort(cmp);
            tempDestinationLanguages.Sort(cmp);
            lstSourceLanguage.Items.Clear();
            lstDestinationLanguage.Items.Clear();
            lstSourceLanguage.Items.AddRange(tempSourceLanguages.ToArray());
            lstDestinationLanguage.Items.AddRange(tempDestinationLanguages.ToArray());

            lstBundles.Items.Clear();
            cmd.CommandText = "SELECT id, name FROM " + db.TablePrefix + "bundles WHERE application = @p";
            reader = cmd.ExecuteReader();
            while(reader.Read()) {
                ListItem itm = new ListItem(reader["name"].ToString(), reader["id"].ToString());
                itm.Selected = true;
                tempBundles.Add(itm);
            }
            tempBundles.Sort(cmp);
            lstBundles.Items.AddRange(tempBundles.ToArray());
            if(lstBundles.Items.Count == 0) {
                ListItem itm = new ListItem(I18nUtils.GetString("UIStrings", "NoBundlesFound"), "");
                itm.Selected = true;
                lstBundles.Items.Add(itm);
                lstBundles.Enabled = false;
            } else {
                lstBundles.Enabled = true;
            }
            reader.Close();

            if(lstSourceLanguage.Items.Count == 0) {
                lstSourceLanguage.Items.Add(new ListItem(I18nUtils.GetString("UIStrings", "NoLanguagesFound"), ""));
                lstSourceLanguage.Enabled = false;
                lstDestinationLanguage.Items.Add(new ListItem(I18nUtils.GetString("UIStrings", "NoLanguagesFound"), ""));
                lstDestinationLanguage.Enabled = false;
            } else {
                lstSourceLanguage.Enabled = true;
                lstDestinationLanguage.Enabled = true;
                lstDestinationLanguage.SelectedIndex = 0;
                if(!string.IsNullOrEmpty(defaultLanguageCode)) {
                    lstSourceType.SelectedValue = defaultLanguageCode;
                }
            }
            btnStartTranslation.Enabled = lstBundles.Enabled && lstSourceLanguage.Enabled;
        }
        db.CloseConnection();
    }

    /// <summary>
    /// Checks a source and destinanation language have been selected and they are different
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.Web.UI.WebControls.ServerValidateEventArgs"/> instance containing the event data.</param>
    protected void vldLanguages_ServerValidate(object sender, ServerValidateEventArgs e) {
        if(lstSourceLanguage.SelectedItem == null) {
            vldLanguages.ErrorMessage = I18nUtils.GetString("Errors", "SourceLanguageRequired");
            e.IsValid = false;
        } else if(lstDestinationLanguage.SelectedItem == null) {
            vldLanguages.ErrorMessage = I18nUtils.GetString("Errors", "DestinationLanguageRequired");
            e.IsValid = false;
        } else if(lstSourceLanguage.SelectedItem.Value == lstDestinationLanguage.SelectedItem.Value || lstSourceType.SelectedItem.Value == lstDestinationType.SelectedItem.Value) {
            vldLanguages.ErrorMessage = I18nUtils.GetString("Errors", "DifferentSourceDestinationLangsRequired");
            e.IsValid = false;
        }
    }

    /// <summary>
    /// Starts a thread to begin translation
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
    protected void btnStartTranslation_Click(object sender, EventArgs e) {
        int minWorkerThreads,
            minIOThreads,
            maxWorkerThreads,
            maxIOThreads;
        ThreadPool.GetMinThreads(out minWorkerThreads, out minIOThreads);
        ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxIOThreads);
        int minThreadsInUse = Math.Min(2, minWorkerThreads),
            maxThreadsInUse = Math.Max(maxWorkerThreads, 10);
        if(Page.IsValid) {
            ThreadPool.SetMinThreads(minThreadsInUse, minIOThreads);
            ThreadPool.SetMaxThreads(maxThreadsInUse, maxIOThreads);
            sourceLanguage = lstSourceLanguage.SelectedValue;
            sourceType = lstSourceType.SelectedValue;
            destinationLanguage = lstDestinationLanguage.SelectedValue;
            destinationType = lstDestinationType.SelectedValue;
            overwriteExisting = radIfExists.SelectedValue == "overwrite";
            verbose = chkVerbose.Checked;
            imgProgress.Visible = true;
            lstProgress.Items.Clear();
            List<ListItem> log = new List<ListItem>();
            log.Add(new ListItem(I18nUtils.GetString("UIStrings", "Starting"))); // the log is deserialized on every timer tick
            Session["log"] = log;
            Session["workerDone"] = bool.FalseString;
            Thread worker = new Thread(new ThreadStart(StartTranslation));
            worker.Name = "TranslationWorker";
            worker.IsBackground = true;
            worker.Start();
            Session["worker"] = worker;
            tmrProgress.Enabled = true;
            btnStartTranslation.Enabled = false;
            mltProgress.ActiveViewIndex = 1;
        }
    }

    /// <summary>
    /// Performs the translation
    /// </summary>
    private void StartTranslation() {
        StatusMessage.StatusChangedEventHandler logHandler = new StatusMessage.StatusChangedEventHandler((statusMessage) => {
            LogText(statusMessage.Message, statusMessage.MessageType == StatusMessage.StatusMessageType.Error);
        });
        try {
            if(db == null || !db.Connected) {
                db = new Database();
                db.OpenConnection();
            }
            using(Command cmd = db.CreateCommand()) {
                List<string> bundles = new List<string>();
                foreach(ListItem itm in lstBundles.Items) {
                    if(itm.Selected) {
                        bundles.Add(itm.Value);
                    }
                }
                List<string> applications = new List<string>();
                foreach(ListItem itm in radApplication.Items) {
                    if(itm.Selected) {
                        applications.Add(itm.Value);
                    }
                }
                // first select COUNT(*) and don't group, then select two fields and group
                string selectQuery = string.Format(@"SELECT {{0}} FROM {0}elements AS e INNER JOIN {0}resources AS r ON r.resourceid = e.resourceid 
                                                     WHERE{1} AND language = @srcLang{2}{3} {{1}}", db.TablePrefix,
                                                    db.GenerateInClause("applicationid", applications.ToArray(), false),
                                                    db.GenerateInClause("bundle", bundles.ToArray(), true),
                                                    overwriteExisting ? "" : string.Format(" AND r.resourceid NOT IN (SELECT resourceid FROM {0}resources WHERE language = @destLang)", db.TablePrefix));
                cmd.AddParameterWithValue("@srcLang", sourceLanguage);
                cmd.AddParameterWithValue("@destLang", destinationLanguage);

                cmd.CommandText = string.Format(selectQuery, "COUNT(*)", "");
                int numResources = int.Parse(cmd.ExecuteScalar().ToString());
                logHandler(new StatusMessage(I18nUtils.GetString("UIStrings", "FoundNumResourcesToTranslate", numResources.ToString()), StatusMessage.StatusMessageType.Status));

                cmd.CommandText = string.Format(selectQuery, "r.resourceid, r.translation", "GROUP BY r.resourceid, r.translation");
                DbDataReader reader = cmd.ExecuteReader();
                int i = 0;
                lock(resources) {
                    resources.Clear();
                }
                while(reader.Read()) {
                    int resourceId = -1;
                    try {
                        resourceId = int.Parse(reader[0].ToString());
                        lock(resources) {
                            resources.Add(resourceId, new PendingTranslation(logHandler, i, reader[1].ToString()));
                        }
                    } catch(Exception ex) {
                        logHandler(new StatusMessage(I18nUtils.GetString("Errors", "ErrorParsingResourceNumDesc", resourceId.ToString(), ex.Message), StatusMessage.StatusMessageType.Error));
                    }
                    i++;
                }

                logHandler(new StatusMessage(I18nUtils.GetString("UIStrings", "QueueingTranslations"), StatusMessage.StatusMessageType.Status));

                translationsDone = new ManualResetEvent[resources.Count];
                foreach(KeyValuePair<int, PendingTranslation> resource in resources) {
                    if(ThreadPool.QueueUserWorkItem(new WaitCallback(DoTranslation), resource.Key)) {
                        translationsDone[resource.Value.WaitIndex] = new ManualResetEvent(false);
                    } else {
                        translationsDone[resource.Value.WaitIndex] = new ManualResetEvent(true);
                        logHandler(new StatusMessage(I18nUtils.GetString("Errors", "QueuingFailedForResourceNum", resource.Key.ToString()), StatusMessage.StatusMessageType.Error));
                    }
                }
                reader.Close();
                if(resources.Count == 0) {
                    throw new Exception(I18nUtils.GetString("UIStrings", "NoResourcesFoundTranslate"));
                }

                translationsDone = new ManualResetEvent[i]; // sometimes there are mismatches if something failed, and the initial count is only a visual guide anyway
                for(int j = 0; j < i; j++) {
                    translationsDone[j] = new ManualResetEvent(false);
                }
            }
            db.CloseConnection(); // translating could take a while and there's no point tying up a DB connection
            WaitHandle.WaitAll(translationsDone);
            logHandler(new StatusMessage("\n" + I18nUtils.GetString("UIStrings", "TranslationDoneStoring"), StatusMessage.StatusMessageType.Status));

            db.OpenConnection();
            using(DbTransaction trans = db.BeginTransaction()) {
                using(Command cmd = db.CreateCommand(trans)) {
                    int numCopied = 0;
                    string querySuccessMessage,
                           queryErrorMessage;
                    string sourceEnglishName,
                           destinationEnglishName;
                    try {
                        sourceEnglishName = CultureInfo.GetCultureInfo(sourceLanguage).EnglishName;
                    } catch {
                        sourceEnglishName = sourceLanguage;
                    }
                    try {
                        destinationEnglishName = CultureInfo.GetCultureInfo(destinationLanguage).EnglishName;
                    } catch {
                        destinationEnglishName = destinationLanguage;
                    }
                    if(overwriteExisting) {
                        switch(db.InstanceType) {
                            case DatabaseType.MySQL:
                                cmd.CommandText = string.Format("INSERT INTO {0}resources (resourceid, language, translation, translatorcomment) VALUES (@tID, @lang, @trans, @comment) ON DUPLICATE KEY UPDATE translation = @trans, translatorcomment = @comment", db.TablePrefix);
                                break;
                            case DatabaseType.SQLServer:
                                cmd.CommandText = string.Format("UPDATE {0}resources SET translation = @trans, translatorcomment = @comment WHERE resourceid = @tID AND language = @lang; IF @@ROWCOUNT = 0 INSERT INTO {0}resources (resourceid, language, translation, translatorcomment) VALUES (@tID, @lang, @trans, @comment)", db.TablePrefix);
                                break;
                            case DatabaseType.SQLite:
                                cmd.CommandText = string.Format("INSERT OR REPLACE INTO {0}resources (resourceid, language, translation, translatorcomment) VALUES (@tID, @lang, @trans, @comment)", db.TablePrefix);
                                break;
                        }
                        querySuccessMessage = I18nUtils.GetString("UIStrings", "OverwritingTranslationResourceNum");
                        queryErrorMessage = I18nUtils.GetString("Errors", "ErrorAddTranslationResourceNum");
                    } else {
                        cmd.CommandText = string.Format("INSERT INTO {0}resources (resourceid, language, translation, translatorcomment) VALUES (@tID, @lang, @trans, @comment)", db.TablePrefix);
                        querySuccessMessage = I18nUtils.GetString("UIStrings", "AddingTranslationResourceNum");
                        queryErrorMessage = I18nUtils.GetString("UIStrings", "TranslationExistsSkippingNum"); // errors thrown when a translation exists
                    }
                    cmd.AddParameterWithValue("@lang", destinationLanguage);
                    cmd.AddParameterWithValue("@comment", I18nUtils.GetString("UIStrings", "AutomaticallyTranslatedUsing", "Google"));
                    cmd.AddParameterWithValue("@tID", string.Empty);
                    cmd.AddParameterWithValue("@trans", string.Empty);

                    foreach(KeyValuePair<int, PendingTranslation> resource in resources) {
                        cmd.Parameters["@tID"].Value = resource.Key;
                        GoogleTranslator.TranslationResponse response = resource.Value.Translation;
                        if(response.Status == 200) { // any errors should have been logged during translation
                            try {
                                cmd.Parameters["@trans"].Value = response.Data.TranslatedText;
                                cmd.ExecuteNonQuery();
                                logHandler(new StatusMessage(string.Format(querySuccessMessage, resource.Key), StatusMessage.StatusMessageType.Success));
                                numCopied++;
                            } catch(Exception ex) {
                                logHandler(new StatusMessage(string.Format(queryErrorMessage, resource.Key, ex.Message), overwriteExisting ? StatusMessage.StatusMessageType.Success : StatusMessage.StatusMessageType.Error));
                            }
                        }
                    }
                    logHandler(new StatusMessage("\n" + I18nUtils.GetString("UIStrings", "MachineTranslateSummary", numCopied.ToString(), sourceEnglishName, GoogleTranslator.GoogleSupportedLanguages[sourceType], destinationEnglishName, GoogleTranslator.GoogleSupportedLanguages[destinationType]), StatusMessage.StatusMessageType.Status));
                }
                trans.Commit();
            }
            Session["workerDone"] = bool.TrueString;
        } catch(Exception ex) {
            logHandler(new StatusMessage(ex.Message, StatusMessage.StatusMessageType.Error));
            Session["workerDone"] = bool.TrueString;
        } finally {
            if(db != null) {
                try {
                    db.CloseConnection();
                } catch { }
            }
            ThreadPool.SetMinThreads(minWorkerThreads, maxIOThreads);
            ThreadPool.SetMaxThreads(maxWorkerThreads, maxIOThreads);
        }
        Thread worker = Session["worker"] as Thread;
        if(worker != null) {
            worker.Abort();
        }
    }

    /// <summary>
    /// Translates a PendingTranslation.
    /// </summary>
    /// <param name="obj">The PendingTranslation.</param>
    private static void DoTranslation(object obj) {
        int resourceId = (int)obj;
        PendingTranslation translation;
        lock(resources) {
            translation = resources[resourceId];
        }
        try {
            GoogleTranslator.TranslationResponse response = GoogleTranslator.Translate(translation.NativeText, sourceType, destinationType);
            translation.Translation = response;
            if(response.Status == 200) {
                translation.LogHandler(new StatusMessage(I18nUtils.GetString("UIStrings", "TranslatedResourceNum", resourceId.ToString()), StatusMessage.StatusMessageType.Success));
                if(verbose) {
                    translation.LogHandler(new StatusMessage(string.Format("\t{0} => {1}", Utils.TrimText(translation.NativeText, 30), Utils.TrimText(response.Data.TranslatedText, 30)), StatusMessage.StatusMessageType.Other));
                }
            } else {
                translation.LogHandler(new StatusMessage(I18nUtils.GetString("Errors", "ErrorTranslatingResourceNumDesc", resourceId.ToString(), response.Status.ToString(), response.Details), StatusMessage.StatusMessageType.Error));
            }
            Thread.Sleep(1000);
        } finally {
            translationsDone[translation.WaitIndex].Set();
        }
    }

    /// <summary>
    /// Logs text to the progress BulletedList.
    /// </summary>
    /// <param name="text">The text to log.</param>
    /// <param name="error"><c>true</c> if the 'error' CSS class should be applied to the text.</param>
    private void LogText(string text, bool error) {
        List<ListItem> log = Session["log"] as List<ListItem>;
        ListItem itm = new ListItem(text);
        if(error) {
            itm.Attributes.Add("class", "error");
        }
        log.Add(itm);
    }

    /// <summary>
    /// Deserializes the log from the Session cache and re-populates the progress BulletedList.
    /// Detects completion of the operation and stops the timer.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
    protected void tmrProgress_Tick(object sender, EventArgs e) {
        try {
            List<ListItem> log = Session["log"] as List<ListItem>;
            lstProgress.Items.Clear();
            if(log == null) {
                log = new List<ListItem>();
            } else {
                lstProgress.Items.AddRange(log.ToArray());
            }
            if(bool.Parse(Session["workerDone"].ToString())) {
                (sender as System.Web.UI.Timer).Enabled = false;
                Session.Remove("workerFile");
                Session.Remove("workerDone");
                imgProgress.Visible = false;
            }
        } catch {
            lstProgress.Items.Add(I18nUtils.GetString("Errors", "ErrorCommunicateServerAborting"));
            Session.Remove("workerFile");
            Session["workerDone"] = bool.TrueString;
        }
    }
    
    /// <summary>
    /// A machine translation queued for execution by the threadpool.
    /// </summary>
    private class PendingTranslation {
        /// <summary>
        /// Gets or sets the log delegate.
        /// </summary>
        /// <value>The delegate used to log messages.</value>
        public StatusMessage.StatusChangedEventHandler LogHandler { get; private set; }

        /// <summary>
        /// Gets or sets the index of this translation in the array of reset events.
        /// </summary>
        /// <value>The index of the wait.</value>
        public int WaitIndex { get; private set; }

        /// <summary>
        /// Gets or sets the native text (in the source language).
        /// </summary>
        /// <value>The native text (in the source language).</value>
        public string NativeText { get; private set;  }

        /// <summary>
        /// Gets or sets the translation received from Google.
        /// </summary>
        /// <value>The translation.</value>
        public GoogleTranslator.TranslationResponse Translation { get; set; }

        /// <summary>
        /// Initializes a new <see cref="PendingTranslation"/>.
        /// </summary>
        /// <param name="logHandler">The delegate used to log messages.</param>
        /// <param name="waitIndex">The index of this translation in the array of reset events..</param>
        /// <param name="nativeText">The native text (in the source language).</param>
        public PendingTranslation(StatusMessage.StatusChangedEventHandler logHandler, int waitIndex, string nativeText) {
            this.LogHandler = logHandler;
            this.WaitIndex = waitIndex;
            this.NativeText = nativeText;
        }
    }

}